package com.google.code.p.restrpc.mon.web;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.code.p.restrpc.util.IOUtils;

/**
 * The abstract HttpServlet which processed the static-resource http requests
 * and provided the template callback method to process the dynamic-resources
 * http requests
 * 
 * @author dz
 */
public abstract class AbstractHttpServlet extends HttpServlet {
    
    private final static Log logger = LogFactory.getLog(AbstractHttpServlet.class);
    
    protected String classesRealPath;
    
    protected RequestHandler[] handlers;
    
    protected Map<String, ServiceDispatcher> dispatchers;
    
    protected boolean zeroCopyAvailable = false;
    
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        
        // register the RequestHandler
        List<RequestHandler> handlerList = new ArrayList<RequestHandler>();
        
        handlerList.add(new RequestHandler() {
            
            public boolean support(String fileName) {
                return fileName.endsWith(".jpg");
            }
            
            public void process(String fileName,
                                HttpServletRequest request,
                                HttpServletResponse response) throws ServletException,
                                                             IOException {
                processBinaryRequest(fileName, "image/jpeg", request, response);
            }
            
        });
        
        handlerList.add(new RequestHandler() {
            
            public boolean support(String fileName) {
                return fileName.endsWith(".png");
            }
            
            public void process(String fileName,
                                HttpServletRequest request,
                                HttpServletResponse response) throws ServletException,
                                                             IOException {
                processBinaryRequest(fileName, "image/png", request, response);
            }
            
        });
        
        handlerList.add(new RequestHandler() {
            
            public boolean support(String fileName) {
                return fileName.endsWith(".bmp");
            }
            
            public void process(String fileName,
                                HttpServletRequest request,
                                HttpServletResponse response) throws ServletException,
                                                             IOException {
                processBinaryRequest(fileName, "image/bmp", request, response);
            }
            
        });
        
        handlerList.add(new RequestHandler() {
            
            public boolean support(String fileName) {
                return fileName.endsWith(".css");
            }
            
            public void process(String fileName,
                                HttpServletRequest request,
                                HttpServletResponse response) throws ServletException,
                                                             IOException {
                processTextRequest(fileName,
                                   "text/css;charset=utf-8",
                                   request,
                                   response);
            }
            
        });
        
        handlerList.add(new RequestHandler() {
            
            public boolean support(String fileName) {
                return fileName.endsWith(".js");
            }
            
            public void process(String fileName,
                                HttpServletRequest request,
                                HttpServletResponse response) throws ServletException,
                                                             IOException {
                processTextRequest(fileName,
                                   "text/javascript;charset=utf-8",
                                   request,
                                   response);
            }
        });
        
        handlerList.add(new RequestHandler() {
            
            public boolean support(String fileName) {
                return fileName.endsWith(".htm") || fileName.endsWith(".html");
            }
            
            public void process(String fileName,
                                HttpServletRequest request,
                                HttpServletResponse response) throws ServletException,
                                                             IOException {
                processTextRequest(fileName,
                                   "text/html;charset=utf-8",
                                   request,
                                   response);
            }
        });
        
        handlers = handlerList.toArray(new RequestHandler[] {});
        
        // unzip the jar file
        classesRealPath = config.getServletContext()
                                .getRealPath("WEB-INF/classes");
        try {
            URI fileURI = getServletCodeSourceLocation();
            if (fileURI != null) {
                zeroCopyAvailable = testAndUnzipSelf(fileURI);
            }
        }
        catch (Exception e) {
            logger.error("Unzip the jar file failed!Caused:" + e.getMessage(),
                         e);
            zeroCopyAvailable = false;
        }
        logger.info("'zeroCopyAvailable' set to " + zeroCopyAvailable);
    }
    
    /**
     * @return the .class file or .jar file real path , return null value if any
     *         exception meet.
     */
    protected abstract URI getServletCodeSourceLocation() throws URISyntaxException;
    
    /**
     * @return
     */
    protected abstract String getWebResourcePath();
    
    /**
     * @param fileURI
     * @return true if unzip-action succeed or tested the jar is unziped before
     *         ,otherwise return false
     */
    protected boolean testAndUnzipSelf(URI fileURI) throws URISyntaxException,
                                                   IOException {
        if (fileURI == null) {
            return false;
        }
        String fullPath = fileURI.toString();
        logger.debug("Resolved fileURI:" + fullPath);
        if (fullPath.endsWith(".jar")) {
            File jarFile = new File(new URI(fullPath));
            if (jarFile.exists()) {
                IOUtils.unzip(new JarFile(jarFile),
                              classesRealPath,
                              getWebResourcePath());
                logger.debug("The jarfile " + jarFile
                             + " has been unziped to "
                             + classesRealPath);
                return true;
            }
        }
        else if (fullPath.endsWith(".class")) {
            if (fullPath.startsWith("jar:")) {
                int lastIndex = fullPath.lastIndexOf(".jar!");
                
                fullPath = fullPath.substring(4, lastIndex + 4);
                logger.debug("Calculated fileURI:" + fullPath);
                File jarFile = new File(new URI(fullPath));
                if (jarFile.exists()) {
                    IOUtils.unzip(new JarFile(jarFile),
                                  classesRealPath,
                                  getWebResourcePath());
                    logger.debug("The jarfile " + jarFile
                                 + " has been unziped to "
                                 + classesRealPath);
                    return true;
                }
            }
            else {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException,
                                                                                 IOException {
        String contextPath = request.getContextPath();
        String servletPath = request.getServletPath();
        String requestURI = request.getRequestURI();
        
        if (contextPath == null) { // root context
            contextPath = "";
        }
        
        String uri = contextPath + servletPath;
        String path = requestURI.substring(contextPath.length() + servletPath.length());
        
        if ("".equals(path) || "/".equals(path)) {
            response.sendRedirect(uri + "/index.html");
            return;
        }
        
        if (isDynamicRequest(path, request, response)) {
            serviceDynamicRequest(path, request, response);
            return;
        }
        
        // find file in resources path
        serviceStaticRequest(path, request, response);
    }
    
    /**
     * test whether the requestPath ( full request path - context path - serlvet
     * path) is a dynamic request or not
     * 
     * @param requestPath
     * @param request
     * @param response
     * @return
     * @throws ServletException
     * @throws IOException
     */
    protected abstract boolean isDynamicRequest(String requestPath,
                                                HttpServletRequest request,
                                                HttpServletResponse response) throws ServletException,
                                                                             IOException;
    
    /**
     * handle the dynamic request
     * 
     * @param requestPath
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    protected abstract void serviceDynamicRequest(String requestPath,
                                                  HttpServletRequest request,
                                                  HttpServletResponse response) throws ServletException,
                                                                               IOException;
    
    /**
     * @param fileName
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    protected void serviceStaticRequest(String fileName,
                                        HttpServletRequest request,
                                        HttpServletResponse response) throws ServletException,
                                                                     IOException {
        for (RequestHandler handler : handlers) {
            if (handler.support(fileName)) {
                handler.process(fileName, request, response);
                return;
            }
        }
        
        // if unsupported static-resource request meeted, just send 403 code to
        // client
        logger.warn("Bad Request:unsupported " + fileName);
        response.setStatus(403);
    }
    
    /**
     * @return
     */
    protected boolean isZeroCopyAvailable() {
        return this.zeroCopyAvailable;
    }
    
    /**
     * @param fileName
     * @param contentType
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    protected void processBinaryRequest(String fileName,
                                        String contentType,
                                        HttpServletRequest request,
                                        HttpServletResponse response) throws ServletException,
                                                                     IOException {
        if (isZeroCopyAvailable()) {
            processRequestInZeroCopy(fileName, contentType, request, response);
        }
        else {
            processBinaryRequestInStreamCopy(fileName,
                                             contentType,
                                             request,
                                             response);
        }
    }
    
    /**
     * @param fileName
     * @param contentType
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    protected void processTextRequest(String fileName,
                                      String contentType,
                                      HttpServletRequest request,
                                      HttpServletResponse response) throws ServletException,
                                                                   IOException {
        if (isZeroCopyAvailable()) {
            processRequestInZeroCopy(fileName, contentType, request, response);
        }
        else {
            processTextRequestInStreamCopy(fileName,
                                           contentType,
                                           request,
                                           response);
        }
    }
    
    /**
     * @param fileName
     * @param contentType
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    protected void processBinaryRequestInStreamCopy(String fileName,
                                                    String contentType,
                                                    HttpServletRequest request,
                                                    HttpServletResponse response) throws ServletException,
                                                                                 IOException {
        
        String filePath = null;
        if (fileName.startsWith("/")) {
            filePath = getWebResourcePath() + fileName;
        }
        else {
            filePath = getWebResourcePath() + "/" + fileName;
        }
        
        InputStream is = getResourceAsStream(filePath);
        if (is == null) {
            response.setStatus(404);
            return;
        }
        
        try {
            IOUtils.copy(is, response.getOutputStream());
        }
        finally {
            IOUtils.close(is);
        }
    }
    
    /**
     * @param fileName
     * @param contentType
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    protected void processTextRequestInStreamCopy(String fileName,
                                                  String contentType,
                                                  HttpServletRequest request,
                                                  HttpServletResponse response) throws ServletException,
                                                                               IOException {
        String filePath = null;
        if (fileName.startsWith("/")) {
            filePath = getWebResourcePath() + fileName;
        }
        else {
            filePath = getWebResourcePath() + "/" + fileName;
        }
        
        try {
            URI fileURI = getServletCodeSourceLocation();
            String fullPath = fileURI.toString();
            if (fullPath.endsWith(".jar")) {
                File jarFile = new File(fileURI);
                if (jarFile.exists()) {
                    long ifModifiedSince = request.getDateHeader("If-Modified-Since");
                    if ((jarFile.lastModified() / 1000) <= (ifModifiedSince / 1000)) {
                        response.setStatus(304);
                        return;
                    }
                    response.setDateHeader("Last-Modified",
                                           jarFile.lastModified());
                }
            }
        }
        catch (URISyntaxException e) {
        }
        
        InputStream is = getResourceAsStream(filePath);
        if (is == null) {
            response.setStatus(404);
            return;
        }
        
        Reader reader = null;
        try {
            reader = new InputStreamReader(is);
            IOUtils.copy(reader, response.getWriter());
        }
        finally {
            IOUtils.close(reader);
            IOUtils.close(is);
        }
    }
    
    /**
     * Zero Copy
     * 
     * @param fileName
     * @param contentType
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    protected void processRequestInZeroCopy(String fileName,
                                            String contentType,
                                            HttpServletRequest request,
                                            HttpServletResponse response) throws ServletException,
                                                                         IOException {
        String filePath = null;
        
        if (fileName.startsWith("/")) {
            filePath = getWebResourcePath() + fileName;
        }
        else {
            filePath = getWebResourcePath() + "/" + fileName;
        }
        
        File file = new File(classesRealPath, filePath);
        if (!file.exists()) {
            response.setStatus(404);
            return;
        }
        
        long ifModifiedSince = request.getDateHeader("If-Modified-Since");
        if ((file.lastModified() / 1000) <= (ifModifiedSince / 1000)) {
            response.setStatus(304);
            return;
        }
        response.setDateHeader("Last-Modified", file.lastModified());
        
        response.setContentType(contentType);
        FileChannel fileChannel = new FileInputStream(file).getChannel();
        OutputStream out = response.getOutputStream();
        WritableByteChannel outChannel = Channels.newChannel(out);
        fileChannel.transferTo(0, file.length(), outChannel);
        out.flush();
    }

    /**
     * Load the file as stream in current xxx.jar file. The child class should
     * use the ClassLoader of the class within current jar file.
     * 
     * @param filePath
     * @return
     */
    protected InputStream getResourceAsStream(String filePath) {
        return Thread.currentThread()
                     .getContextClassLoader()
                     .getResourceAsStream(filePath);
    }
    
    protected static interface RequestHandler {
        
        public boolean support(String fileName);
        
        public void process(String fileName,
                            HttpServletRequest request,
                            HttpServletResponse response) throws ServletException,
                                                         IOException;
        
    }
}
